#!/usr/bin/env xcrun -sdk macosx swift

//
//  Generate.swift
//  Money
//
//  Created by Daniel Thorpe on 01/11/2015.
//
//

import Foundation

typealias Writer = (String) -> Void
typealias Generator = (Writer) -> Void

let enUS = NSLocale(localeIdentifier: "en_US")

protocol TypeGenerator {
    static var typeName: String { get }
    var displayName: String { get }
}

extension TypeGenerator {

    var name: String {
        return displayName.capitalized(with: Locale(identifier: "en_US"))
            .replacingOccurrences(of: " ", with: "")
            .replacingOccurrences(of: " ", with: "")
            .replacingOccurrences(of: "-", with: "")
            .replacingOccurrences(of: "ʼ", with: "")
            .replacingOccurrences(of: ".", with: "")
            .replacingOccurrences(of: "&", with: "")
            .replacingOccurrences(of: "(", with: "")
            .replacingOccurrences(of: ")", with: "")
            .replacingOccurrences(of: "’", with: "")
    }

    var caseNameValue: String {
        return ".\(name)"
    }

    var protocolName: String {
        return "\(name)\(Self.typeName)Type"
    }
}

/// MARK: - Currency Info

func createMoneyType(forCurrencyCode code: String) -> String {
    return "_Money<Currency.\(code)>"
}

func createExtension(for typename: String, withWriter writer: Writer, content: Generator) {
    writer("extension \(typename) {")
    content(writer)
    writer("}")
}

func createFrontMatter(withWriter writer: Writer) {
    writer("// ")
    writer("// Money, https://github.com/danthorpe/Money")
    writer("// Created by Dan Thorpe, @danthorpe")
    writer("// ")
    writer("// The MIT License (MIT)")
    writer("// ")
    writer("// Copyright (c) 2015 Daniel Thorpe")
    writer("// ")
    writer("// Permission is hereby granted, free of charge, to any person obtaining a copy")
    writer("// of this software and associated documentation files (the \"Software\"), to deal")
    writer("// in the Software without restriction, including without limitation the rights")
    writer("// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell")
    writer("// copies of the Software, and to permit persons to whom the Software is")
    writer("// furnished to do so, subject to the following conditions:")
    writer("// ")
    writer("// The above copyright notice and this permission notice shall be included in all")
    writer("// copies or substantial portions of the Software.")
    writer("// ")
    writer("// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR")
    writer("// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,")
    writer("// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE")
    writer("// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER")
    writer("// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,")
    writer("// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE")
    writer("// SOFTWARE.")
    writer("// ")
    writer("// Autogenerated from build scripts, do not manually edit this file.")
    writer("")
}

func createCurrencyTypes(line: Writer) {
    for code in Locale.isoCurrencyCodes {
        line("")
        line("    /// Currency \(code)")
        line("    public final class \(code): Currency.Base, ISOCurrencyType {")
        line("        /// - returns: lazy shared instance for Currency.\(code)")
        line("        public static var sharedInstance = \(code)(code: \"\(code)\")")
        line("    }")
    }
}

func createMoneyTypes(withWriter writer: Writer) {
    writer("")

    for code in Locale.isoCurrencyCodes {
        writer("/// \(code) Money")
        let name = createMoneyType(forCurrencyCode: code)
        writer("public typealias \(code) = \(name)")
    }
}

/// MARK: - Locale Info

struct Country: Comparable, TypeGenerator, CustomStringConvertible {
    static let typeName = "Country"
    let id: String
    let displayName: String
    var langaugeIds = Set<String>()

    var description: String {
        return "\(self.id): \(displayName) -> \(langaugeIds)"
    }

    init?(id: String) {
        self.id = id
        guard let countryDisplayName = enUS.displayName(forKey: NSLocale.Key.countryCode, value: id) else {
            return nil
        }
        displayName = countryDisplayName
    }
}

struct Language: Comparable, TypeGenerator, CustomStringConvertible {
    static let typeName = "Language"

    let id: String
    let displayName: String
    var countryIds = Set<String>()

    var description: String {
        return "\(id): \(displayName) -> \(countryIds)"
    }

    var languageSpeakingCountryEnumName: String {
        return "\(name)Speaking\(Country.typeName)"
    }

    init?(id: String) {
        self.id = id
        guard let languageDisplayName = enUS.displayName(forKey: NSLocale.Key.languageCode, value: id) else {
            return nil
        }
        displayName = languageDisplayName
    }
}

func ==(lhs: Country, rhs: Country) -> Bool {
    return lhs.id == rhs.id
}

func <(lhs: Country, rhs: Country) -> Bool {
    return lhs.name < rhs.name
}

func ==(lhs: Language, rhs: Language) -> Bool {
    return lhs.id == rhs.id
}

func <(lhs: Language, rhs: Language) -> Bool {
    return lhs.name < rhs.name
}

typealias LanguagesById = Dictionary<String, Language>
typealias CountriesById = Dictionary<String, Country>

struct LocaleInfo {

    let languagesById: LanguagesById
    let countriesById: CountriesById
    let languages: [Language]
    let countries: [Country]
    let languagesWithLessThanTwoCountries: [Language]
    let languagesWithMoreThanOneCountry: [Language]

    init() {
        let localeIDs = NSLocale.availableLocaleIdentifiers
        var _countriesById = CountriesById()
        var _languagesById = LanguagesById()

        for id in localeIDs {
            let locale = NSLocale(localeIdentifier: id)
            let countryId = locale.object(forKey: NSLocale.Key.countryCode) as? String
            let country: Country? = countryId.flatMap { _countriesById[$0] ?? Country(id: $0) }
            let languageId = locale.object(forKey: NSLocale.Key.languageCode) as? String
            let language: Language? = languageId.flatMap { _languagesById[$0] ?? Language(id: $0) }

            if let countryId = countryId, var language = language {
                language.countryIds.insert(countryId)
                _languagesById.updateValue(language, forKey: language.id)
            }

            if let languageId = languageId, var country = country {
                country.langaugeIds.insert(languageId)
                _countriesById.updateValue(country, forKey: country.id)
            }
        }

        self.languagesById = _languagesById
        self.countriesById = _countriesById

        countries = ([Country])(countriesById.values).sorted { $0.langaugeIds.count > $1.langaugeIds.count }
        languages = ([Language])(languagesById.values).sorted { $0.countryIds.count > $1.countryIds.count }

        languagesWithLessThanTwoCountries = languages.filter({ $0.countryIds.count < 2 }).sorted()
        languagesWithMoreThanOneCountry = languages.filter({ $0.countryIds.count > 1 }).sorted()
    }
}

let info = LocaleInfo()

func createLanguageSpeakingCountry(withWriter writer: Writer, language: Language) {
    let name = language.languageSpeakingCountryEnumName

    writer("")
    writer("/**")
    writer(" An enum of countries which speak \(language.displayName).")
    writer("*/")
    writer("public enum \(name): CountryType {")

    let _countries = language.countryIds.sorted().flatMap { info.countriesById[$0] }

    // Write the cases
    writer("")
    for country in _countries {
    writer("    /// \(country.displayName) is a country which speaks \(language.displayName).")
    writer("    case \(country.name)")
    }


    // Write a static constant for all
    let caseNames = _countries.map { $0.caseNameValue }
    let joinedCaseNames = caseNames.joined(separator: ", ")
    writer("")
    writer("    /// - returns: an Array of all the countries which speak \(language.displayName)")
    writer("    public static let all: [\(name)] = [ \(joinedCaseNames) ]")

    writer("")
    writer("    /// - returns: the country identifier of a specific \(language.displayName) speaking country.")
    writer("    public var countryIdentifier: String {")
    writer("        switch self {")

    for country in _countries {
        writer("        case .\(country.name):")
        writer("            return \"\(country.id)\"")
    }

    writer("        }") // End of switch
    writer("    }") // End of var

    writer("}") // End of enum
}

func createLanguageSpeakingCountries(withWriter writer: Writer) {
    writer("")
    writer("// MARK: - Country Types")
    for language in info.languagesWithMoreThanOneCountry {
        createLanguageSpeakingCountry(withWriter: writer, language: language)
    }
}

func createLocale(withWriter writer: Writer) {
    writer("")
    writer("// MARK: - Locale")

    do {
        writer("")
        writer("/**")
        writer("")
        writer("Locale is an enum for type safe representation ")
        writer("of locale identifiers. Its cases are languages ")
        writer("in US English. For languages which are spoken ")
        writer("in more than one country, an associated value ")
        writer("of the country should be provided. For example ")
        writer("")
        writer("```swift")
        writer("let locale: MNYLocale = .French(.France)")
        writer("```")
        writer("*/")
        writer("public enum MNYLocale {")

        for language in info.languages.sorted() {
            writer("")
            if language.countryIds.count > 1 {
                writer("    /**")
                writer("     ### \(language.displayName)")
                writer("    - requires: \(language.languageSpeakingCountryEnumName)")
                writer("    */")
                writer("    case \(language.name)(\(language.languageSpeakingCountryEnumName))")
            }
            else {
                writer("    /// ### \(language.displayName)")
                writer("    case \(language.name)")
            }
        }

        writer("}") // End of enum
    }

    // Add extension for LanguageType protocol
    do {
        writer("")
        writer("/**")
        writer(" Locale conforms to LanguageType.")
        writer("*/")
        writer("extension MNYLocale: LanguageType {")
        writer("")
        writer("    /// - returns: the lanauge identifier as a String.")
        writer("    public var languageIdentifier: String {")
        writer("        switch self {")

        for language in info.languages.sorted() {
            if language.countryIds.count > 1 {
                writer("        case .\(language.name)(_):")
                writer("            return \"\(language.id)\"")
            }
            else {
                writer("        case .\(language.name):")
                writer("            return \"\(language.id)\"")
            }
        }

        writer("        }") // End of switch
        writer("    }") // End of var
        writer("}") // End of extension
    }

    // Add extension for CountryType protocol
    do {
        writer("")
        writer("/**")
        writer(" Locale conforms to CountryType.")
        writer("*/")
        writer("extension MNYLocale: CountryType {")
        writer("")
        writer("    /// - returns: the country identifier as a String.")
        writer("    public var countryIdentifier: String {")
        writer("        switch self {")

        let caseNames = info.languagesWithLessThanTwoCountries.map { $0.caseNameValue }
        let joinedCaseNames = caseNames.joined(separator: ", ")
        writer("        case \(joinedCaseNames):")
        writer("            return \"\"")

        for language in info.languagesWithMoreThanOneCountry {
            writer("        case .\(language.name)(let country):")
            writer("            return country.countryIdentifier")
        }

        writer("        }") // End of switch
        writer("    }") // End of var
        writer("}") // End of extension
    }

    // Add extension for LocaleType protocol
    do {
        writer("")
        writer("extension MNYLocale: LocaleType {")
        writer("    // Uses default implementation")
        writer("}") // End of extension
    }
}

func createLocaleTypes(withWriter writer: Writer) {

    // Create the (Language)SpeakingCountry enum types
    createLanguageSpeakingCountries(withWriter: writer)

    // Create the Locale enum
    createLocale(withWriter: writer)
}

// MARK: - Unit Tests

func createUnitTestImports(withWriter writer: Writer) {
    writer("import XCTest")
    writer("@testable import Money")
}

func createXCTestCaseNamed(withWriter writer: Writer, className: String, content: Generator) {
    writer("")
    writer("class \(className)AutogeneratedTests: XCTestCase {")
    content(writer)
    writer("}")
}

func createTestForCountryIdentifierFromCountryCaseName(withWriter writer: Writer, country: Country) {
    writer("")    
    writer("    func test__country_identifier_for_\(country.name)() {")
    writer("        country = .\(country.name)")
    writer("        XCTAssertEqual(country.countryIdentifier, \"\(country.id)\")")
    writer("    }")
}

func createUnitTestsForLanguageSpeakingCountry(withWriter writer: Writer, language: Language) {
    let name = language.languageSpeakingCountryEnumName
    createXCTestCaseNamed(withWriter: writer, className: name) { line in
        writer("")
        writer("    var country: \(name)!")
        for country in language.countryIds.flatMap({ info.countriesById[$0] }) {
            createTestForCountryIdentifierFromCountryCaseName(withWriter: writer, country: country)
        }
    }
}

func createUnitTestsForLanguageSpeakingCountries(withWriter writer: Writer) {
    writer("")
    writer("// MARK: - Country Types Tests")
    for language in info.languagesWithMoreThanOneCountry {
        createUnitTestsForLanguageSpeakingCountry(withWriter: writer, language: language)
    }
}

func createTestForLanguageIdentifier(withWriter writer: Writer, language: Language, country: Country? = nil) {
    writer("")
    if let country = country {
        writer("    func test__language_identifier_for_\(language.name)_\(country.name)() {")
        writer("        locale = .\(language.name)(\(country.caseNameValue))")
        writer("        XCTAssertEqual(locale.languageIdentifier, \"\(language.id)\")")
        writer("        XCTAssertEqual(locale.localeIdentifier, \"\(language.id)_\(country.id)\")")
        writer("    }")
    }
    else {
        writer("    func test__language_identifier_for_\(language.name)() {")
        writer("        locale = .\(language.name)")
        writer("        XCTAssertEqual(locale.languageIdentifier, \"\(language.id)\")")
        writer("        XCTAssertEqual(locale.localeIdentifier, \"\(language.id)\")")
        writer("    }")
    }
}

func createUnitTestsForLocaleWithLanguage(withWriter writer: Writer, language: Language) {
    if language.countryIds.count < 2 {
        createTestForLanguageIdentifier(withWriter: writer, language: language)
    }
    else {
        for country in language.countryIds.flatMap({ info.countriesById[$0] }) {
            createTestForLanguageIdentifier(withWriter: writer, language: language, country: country)
        }
    }
}

func createUnitTestsForLocale(withWriter writer: Writer) {
    writer("")
    writer("// MARK: - Locale Tests")

    for language in info.languagesWithMoreThanOneCountry {

        createXCTestCaseNamed(withWriter: writer, className: "MNYLocale\(language.name)Language") { writer in
            writer("")
            writer("    var locale: MNYLocale!")

            createUnitTestsForLocaleWithLanguage(withWriter: writer, language: language)
        }
    }

    createXCTestCaseNamed(withWriter: writer, className: "MNYLocale") { line in
        writer("")
        writer("    var locale: MNYLocale!")

        for language in info.languagesWithLessThanTwoCountries {
            createUnitTestsForLocaleWithLanguage(withWriter: writer, language: language)
        }
    }
}

// MARK: - Generators

func generateSourceCode(to outputPath: String) {

    guard let outputStream = OutputStream(toFileAtPath: outputPath, append: false) else {
        fatalError("Unable to create output stream at path: \(outputPath)")
    }

    defer {
        outputStream.close()
    }

    let write: Writer = { str in
        guard let data = str.data(using: String.Encoding.utf8) else {
            fatalError("Unable to encode str: \(str)")
        }
        let _ = data.withUnsafeBytes { outputStream.write($0, maxLength: data.count) }
    }

    let writeLine: Writer = { write("\($0)\n") }

    outputStream.open()
    createFrontMatter(withWriter: writeLine)


    createExtension(for: "Currency", withWriter: writeLine, content: createCurrencyTypes)
    write("\n")
    createMoneyTypes(withWriter: writeLine)
    write("\n")
    createLocaleTypes(withWriter: writeLine)
}

func generateUnitTests(to outputPath: String) {

    guard let outputStream = OutputStream(toFileAtPath: outputPath, append: false) else {
        fatalError("Unable to create output stream at path: \(outputPath)")
    }

    defer {
        outputStream.close()
    }

    let write: Writer = { str in
        guard let data = str.data(using: String.Encoding.utf8) else {
            fatalError("Unable to encode str: \(str)")
        }
        let _ = data.withUnsafeBytes { outputStream.write($0, maxLength: data.count) }
    }

    let writeLine: Writer = { write("\($0)\n") }

    outputStream.open()

    createFrontMatter(withWriter: writeLine)

    createUnitTestImports(withWriter: writeLine)

    createUnitTestsForLanguageSpeakingCountries(withWriter: writeLine)

    createUnitTestsForLocale(withWriter: writeLine)
}

// MARK: - Main()
let process = Process()

let pathToSourceCodeFile = "\(process.currentDirectoryPath)/Sources/Autogenerated.swift"
generateSourceCode(to: pathToSourceCodeFile)

let pathToUnitTestsFile = "\(process.currentDirectoryPath)/Tests/AutogeneratedTests.swift"
generateUnitTests(to: pathToUnitTestsFile)

